fix(ui): stop the 401 polling storm when a session dies mid-visit - #318
Open
stubbi wants to merge 1 commit into
Open
fix(ui): stop the 401 polling storm when a session dies mid-visit#318stubbi wants to merge 1 commit into
stubbi wants to merge 1 commit into
Conversation
An expired session made every polling query on the page burn the full react-query retry budget and then poll again on its own interval, with nothing telling the access gate the session was gone. Production traffic shows 50-120 401s per minute against /api/* in minutes with zero clicks and zero pageviews -- a background tab hammering a dead session while the user sees an app that has quietly stopped loading. 1,547 of these across 19 people in 14 days, ~445 on /api/issues/*/live-runs alone. Two causes, both fixed here: - The app-wide QueryClient had no retry override, so a 401 was retried three times like a transient failure. It is an answer, not a blip. - CloudAccessGate decides "signed out, redirect" from its own session query alone. That query is stale-cached for 30s and otherwise only refetched on window focus, so when a session died mid-visit every other query started failing with no feedback path back to the gate. query-auth-policy gives the QueryClient a retry predicate that treats 401/403 as terminal and leaves every other failure on the stock budget. query-auth-recheck subscribes to the query cache and re-asks the session question once when any other query reports an auth failure, so the gate resolves null and redirects to sign-in instead of leaving a broken page. A burst of 401s from many queries collapses into a single re-check, and the session query's own 401 is ignored so it cannot loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Linked Issues or Issue Description
No public issue exists for this; describing it here per the bug report template.
What happened. In the hosted app, when a session expires while the tab is open, the UI does not send the user to sign-in. It keeps polling every resource endpoint (
live-runs,issues,attention,dashboard,agents,labels,budgets,routines,skills,user-directory), each of which returns 401 and is retried three times, then polls again on its interval.What I expected. One 401, then a redirect to sign-in.
Evidence. Minute-level client telemetry for one affected user:
Fork-wide over 14 days: 1,547 x 401 across 19 people, ~445 of them on
/api/issues/*/live-runs.Not related to #311 — that is a self-hosted
authenticated-mode redirect loop in the SPA's own auth route. This is the cloud path, where/auth/*is served by the gateway. Max auth URL length in production is 140 characters and there are no 414s, so the two do not overlap.What Changed
ui/src/lib/query-auth-policy.ts(new) —isAuthFailure()andshouldRetryQuery(). A 401/403 is an answer, not a transient failure, so it does not consume the retry budget; every other failure keeps react-query's stock 3 attempts.ui/src/lib/query-auth-recheck.ts(new) —installAuthFailureRecheck()subscribes to the query cache and refetches the session query once when any other query reports an auth failure.CloudAccessGatethen resolves a null session and redirects to sign-in exactly as it already does.ui/src/main.tsx— wiresretry: shouldRetryQueryinto theQueryClientdefaults and installs the re-check.Two loop guards are deliberate and tested: the session query's own 401 never triggers a re-check (it is the answer), and a burst of 401s from many queries collapses into a single re-check.
Verification
The re-check tests drive a real
QueryClient— no mocking of the module under test — and assert on the sessionqueryFnactually being called again.Full-suite comparison against a clean
fork/maincheckout on the same machine: 21 test files fail at baseline, 20 with this change applied, and the set of failures is a strict subset. No regressions; the pre-existing failures are the known timezone-dependent and macOS-flaky suites.No screenshots: this changes no rendered output. The visible difference is a redirect to sign-in where the page previously stayed broken, which needs a live expiring session to show.
Risks
Low, but worth a reviewer's eye on one thing: the re-check widens what can trigger a session refetch. A genuine per-resource 403 (for example a company the user may not see) now causes one extra
GET /api/auth/get-session. That resolves to the same signed-in session and changes nothing, so the cost is a single request, not a redirect —CloudAccessGateonly redirects on a definitive null session, which is unchanged by this PR.The retry change is strictly a reduction in requests. Nothing that previously succeeded on retry can regress, because a retried 401 never became a 200.
Model Used
claude-opus-5[1m], 1M context), extended thinking, via the Claude Code CLI harness with tool use (shell, file edits, PostHog MCP for the production telemetry). Change authored agent-assisted.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template#NNN/github.com/paperclipai/paperclipURLs)docs/...,fix/...) and contains no internal Paperclip ticket id or instance-derived details🤖 Generated with Claude Code